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

# Content
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 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
165 =item new Crossfire::Protocol host => ..., port => ...
166
167 =cut
168
169 sub new {
170 my $class = shift;
171 my $self = bless {
172 mapw => 13,
173 maph => 13,
174 max_outstanding => 2,
175 token => "a0",
176 @_
177 }, $class;
178
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 if (0 < sysread $self->{fh}, $buf, 16384, length $buf) {
187 for (;;) {
188 last unless 2 <= length $buf;
189 my $len = unpack "n", $buf;
190 last unless $len + 2 <= length $buf;
191
192 substr $buf, 0, 2, "";
193 $self->feed (substr $buf, 0, $len, "");
194 }
195 } else {
196 $self->feed_eof;
197 }
198 });
199
200 $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 mapinfocmd => 1,
209 plugincmd => 1,
210 extendedTextInfos => 1,
211 spellmon => 1,
212 };
213
214 $self->send ("version 1023 1027 perlclient");
215 $self->send_setup;
216 $self->send ("requestinfo skill_info");
217 $self->send ("requestinfo spell_paths");
218
219 $self
220 }
221
222 sub feed {
223 my ($self, $data) = @_;
224
225 $data =~ s/^(\S+)(?:\s|$)//
226 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 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 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 $self->send_setup;
259 } else {
260 $self->send ("addme");
261 }
262
263 $self->feed_newmap;
264 }
265
266 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 sub feed_addme_success {
281 my ($self, $data) = @_;
282
283 $self->addme_success ($data);
284 }
285
286 sub feed_addme_failure {
287 my ($self, $data) = @_;
288
289 $self->addme_failure ($data);
290 }
291
292 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 =back
309
310 =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
311
312 =over 4
313
314 =item $self->addme_success
315
316 =item $self->addme_failure
317
318 =item $self->eof
319
320 =cut
321
322 sub addme_success { }
323 sub addme_failure { }
324 sub eof { }
325
326 sub feed_face1 {
327 my ($self, $data) = @_;
328
329 my ($num, $chksum, $name) = unpack "nNa*", $data;
330
331 $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 my $face = $self->{face}[$num] = { name => $name, chksum => $chksum };
340
341 if (my $data = $self->face_find ($num, $face)) {
342 $face->{image} = $data;
343 $self->face_update ($num, $face);
344 } else {
345 $self->send_queue ("askface $num");
346 }
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 my ($num, $flags, @faces) = unpack "n*", $data;
359
360 $self->{anim}[$num] = \@faces;
361
362 $self->anim_update ($num);
363 }
364
365 =item $conn->sound_play ($x, $y, $soundnum, $type)
366
367 =cut
368
369 sub sound_play { }
370
371 sub feed_sound {
372 my ($self, $data) = @_;
373
374 $self->sound_play (unpack "ccnC", $data);
375 }
376
377 =item $conn->query ($flags, $prompt)
378
379 =cut
380
381 sub query { }
382
383 sub feed_query {
384 my ($self, $data) = @_;
385
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 }
398
399 =item $conn->drawextinfo ($color, $type, $subtype, $message)
400
401 =item $conn->drawinfo ($color, $text)
402
403 =cut
404
405 sub drawextinfo { }
406
407 sub drawinfo { }
408
409 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 sub feed_drawinfo {
422 my ($self, $data) = @_;
423
424 my ($flags, $text) = split / /, $data, 2;
425
426 utf8::decode $text if utf8::valid $text;
427
428 $self->drawinfo ($flags, $text);
429 }
430
431 =item $conn->player_update ($player)
432
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 =item $conn->stats_update ($stats)
453
454 =cut
455
456 sub stats_update { }
457
458 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 sub feed_stats {
466 my ($self, $data) = @_;
467
468 while (length $data) {
469 my $stat = unpack "C", substr $data, 0, 1, "";
470 my $value;
471
472 if ($stat_32bit{$stat}) {
473 $value = unpack "N", substr $data, 0, 4, "";
474 } 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 my $len = unpack "C", substr $data, 0, 1, "";
478 $value = substr $data, 0, $len, "";
479 } elsif ($stat == CS_STAT_EXP64) {
480 my ($hi, $lo) = unpack "NN", substr $data, 0, 8, "";
481 $value = $hi * 2**32 + $lo;
482 } elsif (($stat >= CS_STAT_SKILLEXP_START && $stat <= CS_STAT_SKILLEXP_END)
483 || ($stat >= CS_STAT_SKILLINFO && $stat < CS_STAT_SKILLINFO + CS_NUM_SKILLS)) {
484 my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, "";
485 $value = [$level, $hi * 2**32 + $lo];
486 } else {
487 $value = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "";
488 }
489
490 $self->{stat}{$stat} = $value;
491 }
492
493 $self->stats_update ($self->{stat});
494 }
495
496 =item $conn->container_add ($id, $item...)
497
498 =item $conn->container_clear ($id)
499
500 =item $conn->item_update ($item)
501
502 =item $conn->item_delete ($item...)
503
504 =cut
505
506 sub container_add { }
507 sub container_clear { }
508 sub item_delete { }
509 sub item_update { }
510
511 sub _del_items {
512 my ($self, @items) = @_;
513
514 for my $item (@items) {
515 delete $self->{item}{$item->{tag}};
516 $self->{container}{$item->{container}} = [
517 grep $_ != $item, @{ $self->{container}{$item->{container}} }
518 ];
519 }
520 }
521
522 sub feed_delinv {
523 my ($self, $data) = @_;
524
525 $self->_del_items (@{ $self->{container}{$data} });
526 $self->container_clear ($data);
527 }
528
529 sub feed_delitem {
530 my ($self, $data) = @_;
531
532 my @items = map $self->{item}{$_}, unpack "N*", $data;
533
534 $self->_del_items (@items);
535 $self->item_delete (@items);
536 }
537
538 sub feed_item2 {
539 my ($self, $data) = @_;
540
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 $weight = unpack "l", pack "L", $weight; # weight can be -1
550
551 utf8::decode $names if utf8::valid $names;
552 my ($name, $name_pl) = split /\x00/, $names;
553
554 my $item = {
555 container => $location,
556 tag => $tag,
557 flags => $flags,
558 weight => $weight,
559 face => $face,
560 name => $name,
561 name_pl => $name_pl,
562 anim => $anim,
563 animspeed => $animspeed * TICK,
564 nrof => $nrof,
565 type => $type,
566 };
567
568 if (my $prev = $self->{item}{$tag}) {
569 $self->_del_items ($prev);
570 $self->item_delete ($prev);
571 }
572
573 $self->{item}{$tag} = $item;
574 push @{ $self->{container}{$location} }, $item;
575 push @items, $item;
576 }
577
578 $self->container_add ($location, \@items);
579 }
580
581 sub feed_upditem {
582 my ($self, $data) = @_;
583
584 my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
585
586 if (($flags & UPD_WEIGHT) and ($tag == $self->{player}{tag})) {
587 return; # ignore player updates
588 }
589
590 my $item = $self->{item}{$tag};
591
592 if ($flags & UPD_LOCATION) {
593 $self->item_delete ($item);
594 $self->{container}{$item->{container}} = [
595 grep $_ != $item, @{ $self->{container}{$item->{container}} }
596 ];
597
598 $item->{container} = unpack "N", substr $data, 0, 4, "";
599
600 push @{ $self->{container}{$item->{container}} }, $item;
601 $self->container_add ($item->{location}, $item);
602 }
603
604 $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
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
616 $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
620 $self->item_update ($item);
621 }
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 $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
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 }
707
708 sub feed_map1a {
709 my ($self, $data) = @_;
710
711 my $map = $self->{map} ||= [];
712
713 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 unshift @$_, (undef) x -$self->{mapy} for @$map;
755 $self->{mapy} = 0;
756 }
757
758 if ($self->{mapx} < 0) {
759 unshift @$map, (undef) x -$self->{mapx};
760 $self->{mapx} = 0;
761 }
762
763 $self->map_scroll ($dx, $dy);
764 }
765
766 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 $x = (($coord >> 10) & 63) + $self->{mapx};
773 $y = (($coord >> 4) & 63) + $self->{mapy};
774
775 $cell = $map->[$x][$y] ||= [];
776
777 if ($coord & 15) {
778 @$cell = () if $cell->[0] < 0;
779
780 $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
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 $self->{delayed_scroll_x} += $dx;
806 $self->{delayed_scroll_y} += $dy;
807
808 $self->map_scroll ($dx, $dy);
809 }
810
811 sub feed_newmap {
812 my ($self) = @_;
813
814 $self->{map} = [];
815 $self->{mapx} = 0;
816 $self->{mapy} = 0;
817
818 delete $self->{delayed_scroll_x};
819 delete $self->{delayed_scroll_y};
820
821 $self->map_clear;
822 }
823
824 sub feed_mapinfo {
825 my ($self, $data) = @_;
826
827 my ($token, @data) = split / /, $data;
828
829 (delete $self->{mapinfo_cb}{$token})->(@data)
830 if $self->{mapinfo_cb}{$token};
831
832 $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 $self->{mapinfo_cb}{$token} = sub {
841 $self->send_queue;
842 $cb->(@_);
843 };
844 $self->send_queue ("mapinfo $token $data");
845 }
846
847 sub feed_image {
848 my ($self, $data) = @_;
849
850 my ($num, $len, $data) = unpack "NNa*", $data;
851
852 $self->send_queue;
853 $self->{face}[$num]{image} = $data;
854 $self->face_update ($num, $self->{face}[$num]);
855
856 my @dirty;
857
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
865 $self->map_update (\@dirty);
866 }
867
868 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 =item $conn->map_change ($mode, ...) [OVERWRITE]
889
890 current <flags> <x> <y> <width> <height> <hashstring>
891
892 =cut
893
894 sub map_info { }
895
896 =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 =item $conn->face_update ($facenum, $facedata) [OVERWRITE]
922
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 =item $conn->face_find ($facenum, $facedata) [OVERWRITE]
931
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 =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 =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 sub send_command {
961 my ($self, $command) = @_;
962
963 utf8::encode $command;
964 $self->send ("command $command");
965 }
966
967 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 if ($self->{outstanding} < $self->{max_outstanding} && @{ $self->{send_queue} }) {
977 ++$self->{outstanding};
978 $self->send (shift @{ $self->{send_queue} });
979 }
980 }
981
982 sub send_setup {
983 my ($self) = @_;
984
985 my $setup = join " ", setup => %{$self->{setup_req}},
986 mapsize => "$self->{mapw}x$self->{maph}";
987
988 $self->send ($setup);
989 }
990
991 =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