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 |