ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol.pm
Revision: 1.17
Committed: Wed Apr 12 21:43:11 2006 UTC (18 years, 1 month ago) by root
Branch: MAIN
Changes since 1.16: +22 -3 lines
Log Message:
queue askface commands instead of sending them in one go

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 =item new Crossfire::Protocol host => ..., port => ...
27
28 =cut
29
30 sub new {
31 my $class = shift;
32 my $self = bless {
33 mapw => 13,
34 maph => 13,
35 max_outstanding => 2,
36 @_
37 }, $class;
38
39 $self->{fh} = new IO::Socket::INET PeerHost => $self->{host}, PeerPort => $self->{port}
40 or die "$self->{host}:$self->{port}: $!";
41 $self->{fh}->blocking (0); # stupid nonblock default
42
43 my $buf;
44
45 $self->{w} = AnyEvent->io (fh => $self->{fh}, poll => 'r', cb => sub {
46 if (sysread $self->{fh}, $buf, 16384, length $buf) {
47 for (;;) {
48 last unless 2 <= length $buf;
49 my $len = unpack "n", $buf;
50 last unless $len + 2 <= length $buf;
51
52 substr $buf, 0, 2, "";
53 $self->feed (substr $buf, 0, $len, "");
54 }
55 } else {
56 delete $self->{w};
57 close $self->{fh};
58 }
59 });
60
61 $self->{setup_req} = {
62 sound => 1,
63 exp64 => 1,
64 map1acmd => 1,
65 itemcmd => 2,
66 darkness => 1,
67 facecache => 1,
68 newmapcmd => 1,
69 extendedTextInfos => 1,
70 };
71
72 $self->send ("version 1023 1027 perlclient");
73 $self->send_setup;
74
75 $self
76 }
77
78 sub feed {
79 my ($self, $data) = @_;
80
81 $data =~ s/^(\S+)(?:\s|$)//
82 or return;
83
84 my $command = "feed_$1";
85
86 # warn "$command\n";#d#]
87 $self->$command ($data);
88 }
89
90 sub feed_version {
91 my ($self, $version) = @_;
92 }
93
94 sub feed_setup {
95 my ($self, $data) = @_;
96
97 $data =~ s/^ +//;
98
99 $self->{setup} = { split / +/, $data };
100
101 my ($mapw, $maph) = split /x/, $self->{setup}{mapsize};
102
103 if ($mapw != $self->{mapw} || $maph != $self->{maph}) {
104 ($self->{mapw}, $self->{maph}) = ($mapw, $maph);
105 $self->send_setup;
106 } else {
107 $self->send ("addme");
108 }
109
110 $self->feed_newmap;
111 }
112
113 sub feed_addme_success {
114 my ($self, $data) = @_;
115 }
116
117 sub feed_addme_failure {
118 my ($self, $data) = @_;
119 # maybe should notify user
120 }
121
122 =back
123
124 =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
125
126 =over 4
127
128 =cut
129
130 sub feed_face1 {
131 my ($self, $data) = @_;
132
133 my ($num, $chksum, $name) = unpack "nNa*", $data;
134
135 my $face = $self->{face}[$num] = { name => $name, chksum => $chksum };
136
137 if (my $data = $self->face_find ($face)) {
138 $face->{image} = $data;
139 $self->face_update ($face);
140 } else {
141 $self->send_queue ("askface $num");
142 }
143 }
144
145 =item $conn->anim_update ($num) [OVERWRITE]
146
147 =cut
148
149 sub anim_update { }
150
151 sub feed_anim {
152 my ($self, $data) = @_;
153
154 my ($num, @faces) = unpack "n*", $data;
155
156 $self->{anim}[$num] = \@faces;
157
158 $self->anim_update ($num);
159 }
160
161 =item $conn->play_sound ($x, $y, $soundnum, $type)
162
163 =cut
164
165 sub sound_play { }
166
167 sub feed_sound {
168 my ($self, $data) = @_;
169
170 $self->sound_play (unpack "CCnC", $data);
171 }
172
173 =item $conn->query ($flags, $prompt)
174
175 =cut
176
177 sub query { }
178
179 sub feed_query {
180 my ($self, $data) = @_;
181
182 my ($flags, $prompt) = split /\s+/, $data, 2;
183
184 if ($flags == 0 && $prompt =~ /^What is your name\?\s+:$/ && length $self->{user}) {
185 $self->send ("reply $self->{user}");
186 } elsif ($flags == 4 && $prompt =~ /^What is your password\?\s+:$/ && length $self->{pass}) {
187 $self->send ("reply $self->{pass}");
188 } elsif ($flags == 4 && $prompt =~ /^Please type your password again\.\s+:$/ && length $self->{pass}) {
189 $self->send ("reply $self->{pass}");
190 } else {
191 $self->query ($flags, $prompt);
192 }
193 }
194
195 =item $conn->drawinfo ($color, $text)
196
197 =cut
198
199 sub drawinfo { }
200
201 sub feed_drawinfo {
202 my ($self, $data) = @_;
203
204 $self->drawinfo (split / /, $data, 2);
205 }
206
207 =item $conn->player_update ($player)
208
209 tag, weight, face, name
210
211 =cut
212
213 sub player_update { }
214
215 sub feed_player {
216 my ($self, $data) = @_;
217
218 my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data;
219
220 $self->player_update ($self->{player} = {
221 tag => $tag,
222 weight => $weight,
223 face => $face,
224 name => $name,
225 });
226 }
227
228 =item $conn->stats_update ($stats)
229
230 =cut
231
232 sub stats_update { }
233
234 sub feed_stats {
235 my ($self, $data) = @_;
236
237 while (length $data) {
238 my $stat = unpack "C", substr $data, 0, 1, "";
239 my $value;
240
241 if ($stat == 26 || $stat == 29 || $stat == 30 || $stat == 31 || $stat == 11) {
242 $value = unpack "N", substr $data, 0, 4, "";
243 } elsif ($stat == 17 || $stat == 19) {
244 $value = (1 / 100000) * unpack "N", substr $data, 0, 4, "";
245 } elsif ($stat == 20 || $stat == 21) {
246 my $len = unpack "C", substr $data, 0, 1, "";
247 $value = substr $data, 0, $len, "";
248 } elsif ($stat == 28) {
249 my ($lo, $hi) = unpack "NN", substr $data, 0, 8, "";
250 $value = $hi * 2**32 + $lo;
251 } elsif ($stat >= 118 && $stat <= 129) {
252 my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, "";
253 $value = [$level, $hi * 2**32 + $lo];
254 } else {
255 $value = unpack "n", substr $data, 0, 2, "";
256 }
257
258 $self->{stat}{$stat} = $value;
259 }
260
261 $self->stats_update ($self->{stat});
262 }
263
264 =item $conn->inventory_clear ($id)
265
266 =cut
267
268 sub inventory_clear { }
269
270 sub feed_delinv {
271 my ($self, $data) = @_;
272
273 $self->inventory_clear ($data);
274
275 delete $self->{inventory}[$data];
276 }
277
278 =item $conn->items_delete ($tag...)
279
280 =cut
281
282 sub items_delete { }
283
284 sub feed_delitem {
285 my ($self, $data) = @_;
286
287 $self->items_delete (unpack "n*", $data);
288 }
289
290 =item $conn->inventory_add ($id, [\%item...])
291
292 =cut
293
294 sub inventory_add {
295 }
296
297 sub feed_item2 {
298 my ($self, $data) = @_;
299
300 my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data;
301
302 my @items;
303
304 while (@values) {
305 my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) =
306 splice @values, 0, 9, ();
307
308 my ($name, $name_pl) = split /\x000/, $names;
309
310 push @items, {
311 tag => $tag,
312 flags => $flags,
313 weight => $weight,
314 face => $face,
315 name => $name,
316 name_pl => $name_pl,
317 anim => $anim,
318 animspeed => $animspeed * 0.120, #???
319 nrof => $nrof,
320 type => $type,
321 };
322 }
323
324 $self->inventory_add ($location, \@items);
325 }
326
327 =item $conn->item_update ($tag)
328
329 =cut
330
331 sub item_update { }
332
333 sub feed_upditem {
334 #todo
335 }
336
337 sub feed_map1a {
338 my ($self, $data) = @_;
339
340 my $map = $self->{map} ||= [];
341
342 my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)};
343
344 if ($dx || $dy) {
345 my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)};
346
347 # TODO: optimise this a lot, or maybe just do it on c-level
348 # set flag to -1 for spaces we scroll out
349 {
350 my @darkness;
351
352 if ($dx > 0) {
353 push @darkness, [$mx, $my, $dx - 1, $mh];
354 } elsif ($dx < 0) {
355 push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh];
356 }
357
358 if ($dy > 0) {
359 push @darkness, [$mx, $my, $mw, $dy - 1];
360 } elsif ($dy < 0) {
361 push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy];
362 }
363
364 for (@darkness) {
365 my ($x0, $y0, $w, $h) = @$_;
366 for my $x ($x0 .. $x0 + $w) {
367 for my $y ($y0 .. $y0 + $h) {
368
369 my $cell = $map->[$x][$y]
370 or next;
371
372 $cell->[0] = -1;
373 }
374 }
375 }
376 }
377
378 # now scroll
379
380 $self->{mapx} += $dx;
381 $self->{mapy} += $dy;
382
383 # shift in new space if moving to "negative indices"
384 if ($self->{mapy} < 0) {
385 unshift @$_, (undef) x -$self->{mapy} for @$map;
386 $self->{mapy} = 0;
387 }
388
389 if ($self->{mapx} < 0) {
390 unshift @$map, (undef) x -$self->{mapx};
391 $self->{mapx} = 0;
392 }
393
394 $self->map_scroll ($dx, $dy);
395 }
396
397 my @dirty;
398 my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell);
399
400 while (length $data) {
401 $coord = unpack "n", substr $data, 0, 2, "";
402
403 $x = (($coord >> 10) & 63) + $self->{mapx};
404 $y = (($coord >> 4) & 63) + $self->{mapy};
405
406 $cell = $map->[$x][$y] ||= [];
407
408 if ($coord & 15) {
409 @$cell = () if $cell->[0] < 0;
410
411 $cell->[0] = $coord & 8
412 ? unpack "C", substr $data, 0, 1, ""
413 : 255;
414
415 $cell->[1] = unpack "n", substr $data, 0, 2, ""
416 if $coord & 4;
417 $cell->[2] = unpack "n", substr $data, 0, 2, ""
418 if $coord & 2;
419 $cell->[3] = unpack "n", substr $data, 0, 2, ""
420 if $coord & 1;
421 } else {
422 $cell->[0] = -1;
423 }
424
425 push @dirty, [$x, $y];
426 }
427
428 $self->map_update (\@dirty);
429 }
430
431 sub feed_map_scroll {
432 my ($self, $data) = @_;
433
434 my ($dx, $dy) = split / /, $data;
435
436 $self->{delayed_scroll_x} += $dx;
437 $self->{delayed_scroll_y} += $dy;
438 }
439
440 sub feed_newmap {
441 my ($self) = @_;
442
443 $self->{map} = [];
444 $self->{mapx} = 0;
445 $self->{mapy} = 0;
446
447 delete $self->{delayed_scroll_x};
448 delete $self->{delayed_scroll_y};
449
450 $self->map_clear;
451 }
452
453 sub feed_image {
454 my ($self, $data) = @_;
455
456 $self->send_queue;
457
458 my ($num, $len, $data) = unpack "NNa*", $data;
459
460 $self->{face}[$num]{image} = $data;
461 $self->face_update ($self->{face}[$num]);
462
463 my @dirty;
464
465 for my $x (0..$self->{mapw} - 1) {
466 for my $y (0..$self->{maph} - 1) {
467 push @dirty, [$x, $y]
468 if grep $_ == $num, @{$self->{map}[$x][$y] || []};
469 }
470 }
471
472 $self->map_update (\@dirty);
473 }
474
475 =item $conn->map_clear [OVERWRITE]
476
477 Called whenever the map is to be erased completely.
478
479 =cut
480
481 sub map_clear { }
482
483 =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE]
484
485 Called with a list of x|y coordinate pairs (as arrayrefs) for cells that
486 have been updated and need refreshing.
487
488 =cut
489
490 sub map_update { }
491
492 =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
493
494 Called whenever the map has been scrolled.
495
496 =cut
497
498 sub map_scroll { }
499
500 =item $conn->face_update ($face) [OVERWRITE]
501
502 Called with the face number of face structure whenever a face image has
503 changed.
504
505 =cut
506
507 sub face_update { }
508
509 =item $conn->face_find ($face) [OVERWRITE]
510
511 Find and return the png image for the given face, or the empty list if no
512 face could be found, in which case it will be requested from the server.
513
514 =cut
515
516 sub face_find { }
517
518 =item $conn->send ($data)
519
520 Send a single packet/line to the server.
521
522 =cut
523
524 sub send {
525 my ($self, $data) = @_;
526
527 $data = pack "na*", length $data, $data;
528
529 syswrite $self->{fh}, $data;
530 }
531
532 sub send_queue {
533 my ($self, $cmd) = @_;
534
535 warn "send_queue<$cmd>$self->{outstanding} <@{ $self->{send_queue} || [] }\n";#d#
536 if (defined $cmd) {
537 push @{ $self->{send_queue} }, $cmd;
538 } else {
539 --$self->{outstanding};
540 }
541
542 if ($self->{outstanding} < $self->{max_outstanding}) {
543 ++$self->{outstanding};
544 $self->send (shift @{ $self->{send_queue} });
545 }
546 }
547
548 sub send_setup {
549 my ($self) = @_;
550
551 my $setup = join " ", setup => %{$self->{setup_req}},
552 mapsize => "$self->{mapw}x$self->{maph}";
553
554 $self->send ($setup);
555 }
556
557 =back
558
559 =head1 AUTHOR
560
561 Marc Lehmann <schmorp@schmorp.de>
562 http://home.schmorp.de/
563
564 Robin Redeker <elmex@ta-sa.org>
565 http://www.ta-sa.org/
566
567 =cut
568
569 1