ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol/Base.pm
Revision: 1.106
Committed: Thu Nov 22 14:40:57 2012 UTC (11 years, 6 months ago) by root
Branch: MAIN
CVS Tags: rel-2_01, HEAD
Changes since 1.105: +1 -3 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 =head1 NAME
2
3 Deliantra::Protocol::Base - client protocol module
4
5 =head1 SYNOPSIS
6
7 use base 'Deliantra::Protocol::Base'; # you have to subclass
8
9 =head1 DESCRIPTION
10
11 Base class to implement a crossfire client.
12
13 =over 4
14
15 =cut
16
17 package Deliantra::Protocol::Base;
18
19 our $VERSION = '1.31';
20
21 use common::sense;
22
23 use AnyEvent;
24 use AnyEvent::Socket ();
25 use AnyEvent::Util ();
26 use Compress::LZF;
27 use Scalar::Util ();
28
29 use Socket ();
30
31 use Deliantra::Protocol::Constants;
32
33 use JSON::XS ();
34
35 =item new Deliantra::Protocol::Base host => ..., port => ..., user => ..., pass => ...
36
37 =cut
38
39 sub new {
40 my $class = shift;
41 my $self = bless {
42 host => "gameserver.deliantra.net",
43 port => "deliantra=13327",
44 mapw => 13,
45 maph => 13,
46 token => "a",
47 s_version => { },
48
49 tilesize => 32,
50 json_coder => (JSON::XS->new->max_size(1e7)->utf8),
51 @_
52 }, $class;
53
54 $self->{fh_guard} = AnyEvent::Socket::tcp_connect $self->{host}, $self->{port}, sub {
55 if (my ($fh) = @_) {
56 $self->{fh} = $fh;
57
58 setsockopt $fh, Socket::IPPROTO_TCP (), Socket::TCP_NODELAY (), 1;
59
60 my $buf;
61 $self->{rw} = AE::io $fh, 0, sub {
62 my $len = sysread $fh, $buf, 16384, length $buf;
63
64 if ($len > 0) {
65 $self->{octets_in} += $len;
66
67 for (;;) {
68 last unless 2 <= length $buf;
69 my $len = unpack "n", $buf;
70 last unless $len + 2 <= length $buf;
71
72 substr $buf, 0, 2, "";
73 $self->feed (substr $buf, 0, $len, "");
74 }
75 } else {
76 $self->feed_eof;
77 }
78 };
79
80 $self->{on_connect}->(1) if $self->{on_connect};
81
82 $self->_drain_wbuf;
83
84 } else {
85 $self->{on_connect}->(0) if $self->{on_connect};
86
87 $self->feed_eof;
88 }
89 };
90
91 $self->{setup} = {
92 map1acmd => 1,
93 itemcmd => 2,
94 mapinfocmd => 1,
95 spellmon => 2,
96 lzf => 1, # supports lzf packet
97 frag => 1, # support fragmented packets
98 %{$self->{setup_req} || {} },
99 };
100
101 $self->send ("version " . $self->{json_coder}->encode ({
102 protver => 1,
103 client => "Deliantra Perl Module [$0]",
104 clientver => $VERSION,
105 perlver => $],
106 osver => $^O,
107 modulever => $VERSION,
108 %{ $self->{c_version} },
109 }));
110
111 $self->addme_wait; # for ext_nonces
112
113 # send initial setup req
114 $self->setup_req (mapsize => "$self->{mapw}x$self->{maph}");
115 $self->setup_req (%{$self->{setup}});
116
117 $self
118 }
119
120 =item my $guard = $con->addme_guard
121
122 Delays an C<addme> until thre guard is destroyed.
123
124 =cut
125
126 sub ext_nonces {
127 my ($self, @nonces) = @_;
128
129 $self->{nonces} = \@nonces;
130 $self->addme_ok;
131 }
132
133 sub addme_wait {
134 ++$_[0]{addme_wait}
135 }
136
137 sub addme_ok {
138 my ($self) = @_;
139
140 return if --$self->{addme_wait};
141
142 # done with negotiation
143
144 my $done_cb = sub {
145 my ($ok, $msg) = @_;
146
147 $self->{on_addme}($ok, $msg)
148 if $self->{on_addme};
149
150 # server is supposed to close the connection on error
151 };
152
153 $self->setup ($self->{setup});
154
155 if ($self->{create_login}) {
156 $self->send_exti_req (create_login => $self->{user}, $self->{pass}, $done_cb);
157 } else {
158 my ($n1, $n2) = @{ $self->{nonces} };
159
160 if (
161 $n1 eq $n2
162 or length $n1 < 32
163 or length $n2 < 32
164 ) {
165 # crypto error, avoid playing oracle
166 return $self->feed_eof;
167 }
168
169 my $pass = Deliantra::Util::auth_pw $self->{pass}, $n1, $n2;
170 $self->send_exti_req (login => $self->{user}, $pass, $done_cb);
171 }
172
173 $self->{addme_success} = 1;
174 $self->addme;
175
176 $self->feed_newmap;
177 }
178
179 # not documented, maybe not so useful
180 sub addme { }
181
182 sub addme_guard {
183 my ($self) = @_;
184
185 $self->addme_wait;
186
187 Scalar::Util::weaken $self;
188 AnyEvent::Util::guard {
189 $self->addme_ok if $self;
190 }
191 }
192
193 sub token {
194 ++$_[0]{token}
195 }
196
197 sub feed {
198 my ($self, $data) = @_;
199
200 eval {
201 $data =~ s/^([^ ]+)(?: |$)//
202 or return;
203
204 my $cb = $self->can ("feed_$1")
205 or return; # ignore unknown commands
206
207 $cb->($self, $data);
208 };
209
210 warn $@ if $@;
211 }
212
213 sub feed_lzf {
214 my ($self, $data) = @_;
215
216 $self->feed (decompress $data);
217 }
218
219 sub feed_frag {
220 my ($self, $data) = @_;
221
222 if (length $data) {
223 $self->{_frag} .= $data;
224 } else {
225 $self->feed (delete $self->{_frag});
226 }
227 }
228
229 sub feed_goodbye {
230 my ($self) = @_;
231
232 # nop
233 }
234
235 sub feed_version {
236 my ($self, $version) = @_;
237
238 if ($version =~ /^(\d+) (\d+) (.*)/) {
239 $self->{s_version} = {
240 sc_version => $1,
241 cs_version => $2,
242 server => $3,
243 };
244 } else {
245 $self->{s_version} = $self->{json_coder}->decode ($version);
246 }
247 }
248
249 sub _drain_wbuf {
250 my ($self) = @_;
251
252 return unless $self->{fh};
253
254 unless ($self->{ww}) {
255 my $cb = sub {
256 my $len = syswrite $self->{fh}, $self->{wbuf};
257
258 $self->{octets_out} += $len;
259
260 substr $self->{wbuf}, 0, $len, "" if $len > 0;
261 delete $self->{ww} unless length $self->{wbuf};
262 };
263
264 # try write immediately, to reduce latency,
265 # and in the common case, also cpu requirements.
266 $cb->();
267
268 # still data, so queue
269 $self->{ww} = AE::io $self->{fh}, 1, $cb
270 if length $self->{wbuf};
271 }
272 }
273
274 =back
275
276 =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
277
278 =over 4
279
280 =item $self->setup_req (key => value, ...)
281
282 Send a setup request for the given setting.
283
284 =item $self->setup_chk ($changed_setup)
285
286 Called when a setup reply is received from the server.
287
288 =item $self->setup ($setup)
289
290 Called after the last setup packet has been received, just before an addme
291 request is sent.
292
293 =cut
294
295 sub setup { }
296
297 sub setup_req {
298 my ($self, %kv) = @_;
299
300 while (my ($k, $v) = each %kv) {
301 $self->{setup_req}{$k} = $v;
302 }
303
304 $self->addme_wait;
305 $self->send ("setup " . JSON::XS::encode_json \%kv);
306 }
307
308 sub setup_chk {
309 my ($self, $setup) = @_;
310
311 if (exists $setup->{smoothing}) {
312 $self->{smoothing} = $setup->{smoothing} > 0;
313 }
314
315 if (exists $setup->{mapsize}) {
316 my ($mapw, $maph) = split /x/, $setup->{mapsize};
317
318 ($self->{mapw}, $self->{maph}) = ($mapw, $maph);
319 }
320 }
321
322 sub feed_setup {
323 my ($self, $data) = @_;
324
325 $data = $self->{json_coder}->decode ($data);
326
327 $self->{setup} = { %{ $self->{setup} }, %$data };
328 $self->setup_chk ($data);
329
330 $self->addme_ok;
331 }
332
333 sub feed_eof {
334 my ($self) = @_;
335
336 delete $self->{wbuf};
337 delete $self->{rw};
338 delete $self->{ww};
339 delete $self->{fh_guard};
340 close delete $self->{fh};
341
342 for my $tag (sort { $b <=> $a } %{ $self->{container} || {} }) {
343 $self->_del_items (values %{ $self->{container}{$tag} });
344 $self->container_clear ($tag);
345 }
346
347 $self->eof;
348 }
349
350 sub feed_goodbye {
351 my ($self) = @_;
352
353 $self->feed_eof;
354 }
355
356 sub logout {
357 my ($self) = @_;
358
359 $self->{fh} or return;
360
361 $self->feed_eof;
362 }
363
364 sub destroy {
365 my ($self) = @_;
366
367 $self->logout;
368
369 %$self = ();
370 }
371
372 =item $self->eof
373
374 =cut
375
376 sub eof { }
377
378 sub feed_face1 {
379 my ($self, $data) = @_;
380
381 my ($num, $chksum, $name) = unpack "nNa*", $data;
382
383 $self->need_face ($num, { name => "$name\x00$chksum", type => 0 });
384 }
385
386 sub feed_fx {
387 my ($self, $data) = @_;
388
389 my $type = 0;
390 my @info = unpack "(w C/a)*", $data;
391 while (@info) {
392 my $facenum = shift @info;
393 my $name = shift @info;
394
395 if ($facenum) {
396 $self->need_face ($facenum, { name => $name, type => $type });
397 } else {
398 $type = unpack "w", $name;
399 }
400 }
401 }
402
403 =item $self->smooth_update ($facenum, $face)
404
405 =cut
406
407 sub smooth_update { }
408
409 sub feed_sx {
410 my ($self, $data) = @_;
411
412 my @info = unpack "(w w w)*", $data;
413 while (@info) {
414 my $level = pop @info;
415 my $smooth = pop @info;
416 my $facenum = pop @info;
417
418 my $face = $self->{face}[$facenum];
419
420 $face->{smoothface} = $smooth;
421 $face->{smoothlevel} = $level;
422
423 $self->smooth_update ($facenum, $face);
424 }
425 }
426
427 sub need_face {
428 my ($self, $num, $face) = @_;
429
430 $face->{loading} = 1;
431
432 $self->{face}[$num] = $face;
433
434 $self->face_find ($num, $face, sub {
435 my ($data) = @_;
436
437 if (length $data) {
438 delete $face->{loading};
439 $face->{data} = $data;
440 $self->face_update ($num, $face, 0);
441 } else {
442 $self->send ("askface $num");
443 }
444 });
445 }
446
447 =item $conn->ask_face ($num, $pri, $data_cb, $finish_cb)
448
449 =cut
450
451 sub ask_face {
452 my ($self, $num, $pri, $data_cb, $finish_cb) = @_;
453
454 $self->{ask_face}{$num} = [$data_cb || undef, $finish_cb || sub { }]
455 if $data_cb || $finish_cb;
456
457 $self->send ($pri ? "askface $num $pri" : "askface $num");
458 }
459
460 =item $conn->anim_update ($num) [OVERWRITE]
461
462 =cut
463
464 sub anim_update { }
465
466 sub feed_anim {
467 my ($self, $data) = @_;
468
469 my ($num, $flags, @faces) = unpack "n*", $data;
470
471 $self->{anim}[$num] = \@faces;
472
473 $self->anim_update ($num);
474 }
475
476 =item $conn->sound_play ($type, $face, $dx, $dy, $volume)
477
478 =cut
479
480 sub sound_play { }
481
482 sub feed_sc {
483 my ($self, $data) = @_;
484
485 $self->sound_play (unpack "CwccC", $_)
486 for unpack "(w/a*)*", $data;
487 }
488
489 =item $conn->query ($flags, $prompt)
490
491 =cut
492
493 sub query { }
494
495 sub feed_query {
496 my ($self, $data) = @_;
497
498 my ($flags, $prompt) = split /\s+/, $data, 2;
499
500 $self->query ($flags, $prompt);
501 }
502
503 =item $conn->msg ($default_color, $type, $text, @extra)
504
505 =cut
506
507 sub msg { }
508
509 sub feed_msg {
510 my ($self, $data) = @_;
511
512 if ("[" eq substr $data, 0, 1) {
513 $self->msg (@{ $self->{json_coder}->decode ($data) });
514 } else {
515 utf8::decode $data;
516 $self->msg (split /\s+/, $data, 3);
517 }
518 }
519
520 =item $conn->ex ($tag, $cb)
521
522 =cut
523
524 sub feed_ex {
525 my ($self, $data) = @_;
526
527 my ($tag, $text) = unpack "wa*", $data;
528 utf8::decode $text;
529
530 if (my $q = delete $self->{cb_ex}{$tag}) {
531 $_->($text, $tag) for @$q;
532 }
533 }
534
535 sub ex {
536 my ($self, $tag, $cb) = @_;
537
538 my $q = $self->{cb_ex}{$tag} ||= [];
539 push @$q, $cb;
540 $self->send ("ex $tag") if @$q == 1;
541 }
542
543 =item $conn->player_update ($player)
544
545 tag, weight, face, name
546
547 =cut
548
549 sub logged_in { }
550
551 sub player_update { }
552
553 sub feed_player {
554 my ($self, $data) = @_;
555
556 delete $self->{sent_login};
557
558 # since the server never sends a "you have logged in" of any kind
559 # we rely on being send "player" only once - after log-in.
560 $self->logged_in;
561
562 my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data;
563
564 $self->player_update ($self->{player} = {
565 tag => $tag,
566 weight => $weight,
567 face => $face,
568 name => $name,
569 });
570 }
571
572 =item $conn->stats_update ($stats)
573
574 =cut
575
576 sub stats_update { }
577
578 my %stat_32bit = map +($_ => 1),
579 CS_STAT_WEIGHT_LIM,
580 CS_STAT_SPELL_ATTUNE,
581 CS_STAT_SPELL_REPEL,
582 CS_STAT_SPELL_DENY,
583 CS_STAT_EXP;
584
585 sub feed_stats {
586 my ($self, $data) = @_;
587
588 while (length $data) {
589 my $stat = unpack "C", substr $data, 0, 1, "";
590 my $value;
591
592 if ($stat_32bit{$stat}) {
593 $value = unpack "N", substr $data, 0, 4, "";
594 } elsif ($stat == CS_STAT_SPEED || $stat == CS_STAT_WEAP_SP) {
595 $value = (1 / FLOAT_MULTF) * unpack "N", substr $data, 0, 4, "";
596 } elsif ($stat == CS_STAT_RANGE || $stat == CS_STAT_TITLE) {
597 my $len = unpack "C", substr $data, 0, 1, "";
598 $value = substr $data, 0, $len, "";
599 utf8::decode $value;
600 } elsif ($stat == CS_STAT_EXP64) {
601 my ($hi, $lo) = unpack "NN", substr $data, 0, 8, "";
602 $value = $hi * 2**32 + $lo;
603 } elsif ($stat >= CS_STAT_SKILLINFO && $stat < CS_STAT_SKILLINFO + CS_NUM_SKILLS) {
604 my ($level, $hi, $lo) = unpack "CNN", substr $data, 0, 9, "";
605 $value = [$level, $hi * 2**32 + $lo];
606 } else {
607 $value = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "";
608 }
609
610 $self->{stat}{$stat} = $value;
611 }
612
613 $self->stats_update ($self->{stat});
614 }
615
616 =item $conn->container_add ($id, $item...)
617
618 =item $conn->container_clear ($id)
619
620 =item $conn->item_update ($item)
621
622 =item $conn->item_delete ($item...)
623
624 =cut
625
626 sub container_add { }
627 sub container_clear { }
628 sub item_delete { }
629 sub item_update { }
630
631 sub _del_items {
632 my ($self, @items) = @_;
633
634 for my $item (@items) {
635 next if $item->{tag} == $self->{player}{tag};
636 delete $self->{container}{$item->{container}}{$item+0};
637 delete $self->{item}{$item->{tag}};
638 }
639 }
640
641 sub feed_delinv {
642 my ($self, $data) = @_;
643
644 $self->_del_items (values %{ $self->{container}{$data} });
645 $self->container_clear ($data);
646 }
647
648 sub feed_delitem {
649 my ($self, $data) = @_;
650
651 my @items = map $self->{item}{$_}, unpack "N*", $data;
652
653 $self->_del_items (@items);
654 $self->item_delete (@items);
655 }
656
657 my $count = 0;
658
659 sub feed_item2 {
660 my ($self, $data) = @_;
661
662 my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data;
663
664 my @items;
665
666 my $NOW = time;
667
668 while (@values) {
669 my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) =
670 splice @values, 0, 9, ();
671
672 $weight = unpack "l", pack "L", $weight; # weight can be -1
673
674 utf8::decode $names;
675 my ($name, $name_pl) = split /\x00/, $names;
676
677 my $item = {
678 container => $location,
679 tag => $tag,
680 flags => $flags,
681 weight => $weight,
682 face => $face,
683 name => $name,
684 name_pl => $name_pl,
685 anim => $anim,
686 animspeed => $animspeed * TICK,
687 nrof => $nrof,
688 type => $type,
689 count => ++$count,
690 mtime => $NOW,
691 ctime => $NOW,
692 };
693
694 if ($tag == $self->{player}{tag}) {
695 $self->player_update ($self->{player} = $item);
696 } else {
697 if (my $prev = $self->{item}{$tag}) {
698 $self->_del_items ($prev);
699 $self->item_delete ($prev);
700 }
701
702 $self->{item}{$tag} = $item;
703 $self->{container}{$location}{$item+0} = $item;
704 push @items, $item;
705 }
706 }
707
708 $self->container_add ($location, \@items);
709 }
710
711 sub feed_upditem {
712 my ($self, $data) = @_;
713
714 my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
715
716 my $item;
717 if ($tag == $self->{player}{tag}) {
718 $item = $self->{player};
719 } else {
720 $item = $self->{item}{$tag}
721 or warn "received item update for unseen item $tag\n";
722 }
723
724 if ($flags & UPD_LOCATION) {
725 $self->item_delete ($item);
726 delete $self->{container}{$item->{container}}{$item+0};
727 $item->{container} = unpack "N", substr $data, 0, 4, "";
728 $self->{container}{$item->{container}}{$item+0} = $item;
729 $self->container_add ($item->{location}, $item);
730 }
731
732 $item->{flags} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FLAGS;
733 $item->{weight} = unpack "l", pack "L", unpack "N", substr $data, 0, 4, "" if $flags & UPD_WEIGHT;
734 $item->{face} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FACE;
735
736 if ($flags & UPD_NAME) {
737 my $len = unpack "C", substr $data, 0, 1, "";
738
739 my $names = substr $data, 0, $len, "";
740 utf8::decode $names;
741 @$item{qw(name name_pl)} = split /\x00/, $names;
742 }
743
744 $item->{anim} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_ANIM;
745 $item->{animspeed} = TICK * unpack "C", substr $data, 0, 1, "" if $flags & UPD_ANIMSPEED;
746 $item->{nrof} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_NROF;
747
748 $item->{mtime} = time;
749
750 if ($item->{tag} == $self->{player}{tag}) {
751 $self->player_update ($self->{player} = $item);
752 } else {
753 $self->item_update ($item);
754 }
755 }
756
757 =item $conn->spell_add ($spell)
758
759 $spell = {
760 tag => ...,
761 minlevel => ...,
762 casting_time => ...,
763 mana => ...,
764 grace => ...,
765 level => ...,
766 skill => ...,
767 path => ...,
768 face => ...,
769 name => ...,
770 };
771
772 =item $conn->spell_update ($spell)
773
774 (the default implementation calls delete then add)
775
776 =item $conn->spell_delete ($spell)
777
778 =cut
779
780 sub spell_add { }
781
782 sub spell_update {
783 my ($self, $spell) = @_;
784
785 $self->spell_delete ($spell);
786 $self->spell_add ($spell);
787 }
788
789 sub spell_delete { }
790
791 sub feed_addspell {
792 my ($self, $data) = @_;
793
794 my @data = unpack "(NnnnnnCNN C/a)*", $data;
795
796 while (@data) {
797 my $spell = {
798 tag => (shift @data),
799 minlevel => (shift @data),
800 casting_time => (shift @data),
801 mana => (unpack "s", pack "S", shift @data),
802 grace => (unpack "s", pack "S", shift @data),
803 level => (unpack "s", pack "S", shift @data),
804 skill => (shift @data),
805 path => (shift @data),
806 face => (shift @data),
807 name => (shift @data),
808 };
809
810 $self->spell_add ($self->{spell}{$spell->{tag}} = $spell);
811 }
812 }
813
814 sub feed_updspell {
815 my ($self, $data) = @_;
816
817 my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
818
819 # only 1, 2, 4 supported
820 # completely untested
821
822 my $spell = $self->{spell}{$tag};
823
824 $spell->{mana} = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_MANA;
825 $spell->{grace} = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_GRACE;
826 $spell->{level} = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_LEVEL; # was UPD_SP_DAMAGE in earlier servers
827
828 $self->spell_update ($spell);
829 }
830
831 sub feed_delspell {
832 my ($self, $data) = @_;
833
834 $self->spell_delete (delete $self->{spell}{unpack "N", $data});
835 }
836
837 =item $conn->magicmap ($w, $h, $px, $py, $data)
838
839 =item $conn->map_change ($type, ...)
840
841 =cut
842
843 sub feed_magicmap {
844 my ($self, $data) = @_;
845
846 my ($w, $h, $x, $y, $data) = split / /, $data, 5;
847
848 $self->magicmap ($w, $h, $x, $y, $data);
849 }
850
851 sub feed_map1a {
852 my ($self, $data) = @_;
853 }
854
855 sub feed_map_scroll {
856 my ($self, $data) = @_;
857
858 # my ($dx, $dy) = split / /, $data;
859 }
860
861 sub feed_newmap {
862 my ($self) = @_;
863
864 $self->map_clear;
865 }
866
867 sub feed_map_scroll {
868 my ($self, $data) = @_;
869
870 my ($dx, $dy) = split / /, $data;
871
872 $self->{delayed_scroll_x} += $dx;
873 $self->{delayed_scroll_y} += $dy;
874
875 $self->map_scroll ($dx, $dy);
876 }
877
878 sub map_change { }
879
880 sub feed_mapinfo {
881 my ($self, $data) = @_;
882
883 my ($token, @data) = split / /, $data;
884
885 (delete $self->{mapinfo_cb}{$token})->(@data)
886 if $self->{mapinfo_cb}{$token};
887
888 $self->map_change (@data) if $token eq "-";
889 }
890
891 sub send_mapinfo {
892 my ($self, $data, $cb) = @_;
893
894 my $token = $self->token;
895
896 $self->{mapinfo_cb}{$token} = $cb;
897 $self->send ("mapinfo $token $data");
898 }
899
900 sub feed_image {
901 my ($self, $data) = @_;
902
903 my ($num, $len, $data) = unpack "NNa*", $data;
904
905 my $face = $self->{face}[$num];
906
907 delete $face->{loading};
908 $face->{data} = $data;
909 $self->face_update ($num, $face, 1);
910
911 $self->map_update;
912 }
913
914 sub feed_ix {
915 my ($self, $data) = @_;
916
917 my ($num, $ofs, $data) = unpack "w w a*", $data;
918
919 my $cbs = $self->{ask_face}{$num};
920
921 if (my $cb = $cbs && $cbs->[0]) {
922 $cb->($num, $ofs, $data);
923 } elsif (!$ofs || length $data) {
924 # avoid stupid substr out of range error
925 $self->{ix_recv_buf}{$num} //= " " x $ofs;
926 substr $self->{ix_recv_buf}{$num}, $ofs, (length $data), $data;
927 $self->{ix_recv_ofs}{$num} = $ofs;
928 } else {
929 # ix with empty data but nonzero offset means to abort the current ix
930 delete $self->{ix_recv_buf}{$num};
931 delete $self->{ix_recv_ofs}{$num};
932 }
933
934 unless ($ofs) {
935 delete $self->{ix_recv_ofs}{$num};
936
937 if ($cbs) {
938 $cbs->[1]->($num, delete $self->{ix_recv_buf}{$num});
939 } else {
940 my $face = $self->{face}[$num];
941
942 delete $face->{loading};
943 delete $face->{cache}; # cache cna be used by the application
944 $face->{data} = delete $self->{ix_recv_buf}{$num};
945 $self->face_update ($num, $face, 1);
946
947 $self->map_update;
948 }
949 }
950 }
951
952 =item $conn->map_change ($mode, ...) [OVERWRITE]
953
954 current <flags> <x> <y> <width> <height> <hashstring>
955
956 =cut
957
958 sub map_info { }
959
960 =item $conn->map_clear [OVERWRITE]
961
962 Called whenever the map is to be erased completely.
963
964 =cut
965
966 sub map_clear { }
967
968 =item $conn->map_update
969
970 Called whenever map data or faces have been received.
971
972 =cut
973
974 sub map_update { }
975
976 =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
977
978 Called whenever the map has been scrolled.
979
980 =cut
981
982 sub map_scroll { }
983
984 =item $conn->face_update ($facenum, $facedata, $changed) [OVERWRITE]
985
986 Called with the face number of face structure whenever a face image
987 becomes known (either because C<face_find> returned it, in which case
988 C<$changed> is false, or because we got an update, in which case
989 C<$changed> is true).
990
991 =cut
992
993 sub face_update { }
994
995 =item $conn->face_find ($facenum, $facedata, $cb) [OVERWRITE]
996
997 Find and pass to the C<$cb> callback the png image data for the given
998 face, or the empty list if no face could be found, in which case it will
999 be requested from the server.
1000
1001 =cut
1002
1003 sub face_find { }
1004
1005 =item $conn->send ($data)
1006
1007 Send a single packet/line to the server.
1008
1009 =cut
1010
1011 sub send {
1012 my ($self, $data) = @_;
1013
1014 $self->{wbuf} .= pack "na*", length $data, $data;
1015 $self->_drain_wbuf;
1016 }
1017
1018 =item $conn->send_utf8 ($data)
1019
1020 Send a single packet/line to the server and encodes it to
1021 utf-8 before sending it.
1022
1023 =cut
1024
1025 sub send_utf8 {
1026 my ($self, $data) = @_;
1027 utf8::encode $data;
1028 $self->send ($data);
1029 }
1030
1031 =item $conn->send_command ($command])
1032
1033 Uses command to send a user-level command to the server. Encodes the
1034 command to UTF-8.
1035
1036 =cut
1037
1038 sub send_command {
1039 my ($self, $command, $cb1, $cb2) = @_;
1040
1041 utf8::encode $command;
1042
1043 $self->send ("command $command");
1044 }
1045
1046 =item $conn->send_pickup ($pickup)
1047
1048 Sets the pickup configuration.
1049
1050 =cut
1051
1052 sub send_pickup {
1053 my ($self, $pickup) = @_;
1054
1055 $self->send_command ("pickup " . ($pickup | PICKUP_NEWMODE));
1056 }
1057
1058 sub connect_ext {
1059 my ($self, $type, $cb) = @_;
1060
1061 $self->{extcmd_cb_type}{$type} = $cb;
1062 }
1063
1064 sub disconnect_ext {
1065 my ($self, $type) = @_;
1066
1067 delete $self->{extcmd_cb_type}{$type};
1068 }
1069
1070 sub feed_ext {
1071 my ($self, $data) = @_;
1072
1073 my ($type, @payload) = eval { @{ $self->{json_coder}->decode ($data) } }
1074 or return;
1075
1076 if (my $cb = $self->{extcmd_cb_id}{$type} || $self->{extcmd_cb_type}{$type}) {
1077 $cb->(@payload)
1078 or delete $self->{extcmd_cb_id}{$type};
1079 } elsif (my $cb = $self->can ("ext_$type")) {
1080 $cb->($self, @payload);
1081 }
1082 }
1083
1084 sub send_ext_msg {
1085 my ($self, $type, @msg) = @_;
1086
1087 $self->send ("ext " . $self->{json_coder}->encode ([$type, 0, @msg]));
1088 }
1089
1090 sub send_exti_msg {
1091 my ($self, $type, @msg) = @_;
1092
1093 $self->send ("exti " . $self->{json_coder}->encode ([$type, 0, @msg]));
1094 }
1095
1096 sub send_ext_req {
1097 my $cb = pop; # callback is last
1098 my ($self, $type, @msg) = @_;
1099
1100 my $id = $self->token;
1101 $self->{extcmd_cb_id}{"reply-$id"} = $cb;
1102 $self->send ("ext " . $self->{json_coder}->encode ([$type, $id, @msg]));
1103 }
1104
1105 sub send_exti_req {
1106 my $cb = pop; # callback is last
1107 my ($self, $type, @msg) = @_;
1108
1109 my $id = $self->token;
1110 $self->{extcmd_cb_id}{"reply-$id"} = $cb;
1111 $self->send ("exti " . $self->{json_coder}->encode ([$type, $id, @msg]));
1112 }
1113
1114 =back
1115
1116 =head1 AUTHOR
1117
1118 Marc Lehmann <schmorp@schmorp.de>
1119 http://home.schmorp.de/
1120
1121 Robin Redeker <elmex@ta-sa.org>
1122 http://www.ta-sa.org/
1123
1124 =cut
1125
1126 1